msg_tool\scripts\hexen_haus\archive/
arcc.rs

1//! HexenHaus ARCC archive (.arc)
2use crate::ext::io::*;
3use crate::scripts::base::*;
4use crate::types::*;
5use crate::utils::encoding::decode_to_string;
6use anyhow::Result;
7use std::io::{Read, Seek, SeekFrom};
8use std::sync::{Arc, Mutex};
9
10#[derive(Debug)]
11/// HexenHaus ARCC archive builder
12pub struct HexenHausArccArchiveBuilder;
13
14impl HexenHausArccArchiveBuilder {
15    /// Creates a new `HexenHausArccArchiveBuilder`
16    pub const fn new() -> Self {
17        HexenHausArccArchiveBuilder
18    }
19}
20
21impl ScriptBuilder for HexenHausArccArchiveBuilder {
22    fn default_encoding(&self) -> Encoding {
23        Encoding::Cp932
24    }
25
26    fn default_archive_encoding(&self) -> Option<Encoding> {
27        Some(Encoding::Cp932)
28    }
29
30    fn build_script(
31        &self,
32        buf: Vec<u8>,
33        _filename: &str,
34        _encoding: Encoding,
35        archive_encoding: Encoding,
36        config: &ExtraConfig,
37        _archive: Option<&Box<dyn Script>>,
38    ) -> Result<Box<dyn Script + Send + Sync>> {
39        Ok(Box::new(HexenHausArccArchive::new(
40            MemReader::new(buf),
41            archive_encoding,
42            config,
43        )?))
44    }
45
46    fn build_script_from_file(
47        &self,
48        filename: &str,
49        _encoding: Encoding,
50        archive_encoding: Encoding,
51        config: &ExtraConfig,
52        _archive: Option<&Box<dyn Script>>,
53    ) -> Result<Box<dyn Script + Send + Sync>> {
54        if filename == "-" {
55            let data = crate::utils::files::read_file(filename)?;
56            return Ok(Box::new(HexenHausArccArchive::new(
57                MemReader::new(data),
58                archive_encoding,
59                config,
60            )?));
61        }
62        let file = std::fs::File::open(filename)?;
63        let reader = std::io::BufReader::new(file);
64        Ok(Box::new(HexenHausArccArchive::new(
65            reader,
66            archive_encoding,
67            config,
68        )?))
69    }
70
71    fn build_script_from_reader<'a>(
72        &self,
73        reader: Box<dyn ReadSeek + Send + Sync + 'a>,
74        _filename: &str,
75        _encoding: Encoding,
76        archive_encoding: Encoding,
77        config: &ExtraConfig,
78        _archive: Option<&Box<dyn Script>>,
79    ) -> Result<Box<dyn Script + Send + Sync + 'a>> {
80        Ok(Box::new(HexenHausArccArchive::new(
81            reader,
82            archive_encoding,
83            config,
84        )?))
85    }
86
87    fn extensions(&self) -> &'static [&'static str] {
88        &["arc"]
89    }
90
91    fn script_type(&self) -> &'static ScriptType {
92        &ScriptType::HexenHausArcc
93    }
94
95    fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
96        if buf_len >= 4 && buf.starts_with(b"ARCC") {
97            Some(10)
98        } else {
99            None
100        }
101    }
102
103    fn is_archive(&self) -> bool {
104        true
105    }
106}
107
108#[derive(Debug, Clone)]
109struct HexenHausArccEntry {
110    name: String,
111    offset: u64,
112    size: u32,
113}
114
115#[derive(Debug)]
116/// HexenHaus ARCC archive
117pub struct HexenHausArccArchive<'a, T: Read + Seek + std::fmt::Debug + 'a> {
118    reader: Arc<Mutex<T>>,
119    entries: Vec<HexenHausArccEntry>,
120    _mark: std::marker::PhantomData<&'a ()>,
121}
122
123impl<'b, T: Read + Seek + std::fmt::Debug + 'b> HexenHausArccArchive<'b, T> {
124    /// Creates a new `HexenHausArccArchive`
125    pub fn new(mut reader: T, archive_encoding: Encoding, _config: &ExtraConfig) -> Result<Self> {
126        reader.seek(SeekFrom::Start(0))?;
127        let mut signature = [0u8; 4];
128        reader.read_exact(&mut signature)?;
129        if signature != *b"ARCC" {
130            return Err(anyhow::anyhow!("Invalid HexenHaus ARCC signature"));
131        }
132        reader.seek(SeekFrom::Start(0))?;
133        let reader = Arc::new(Mutex::new(reader));
134
135        let file_count = reader.cpeek_u32_at(0x14)?;
136        let entry_count = file_count as usize;
137
138        let mut index_offset = 0x2a_u64;
139        let mut tag = [0u8; 4];
140        reader.cpeek_exact_at(index_offset, &mut tag)?;
141        if &tag != b"NAME" {
142            return Err(anyhow::anyhow!("Missing NAME section in ARCC archive"));
143        }
144        let addr_offset = reader.cpeek_u64_at(index_offset + 4)?;
145        index_offset += 0x0e;
146
147        reader.cpeek_exact_at(index_offset, &mut tag)?;
148        if &tag != b"NIDX" {
149            return Err(anyhow::anyhow!("Missing NIDX section in ARCC archive"));
150        }
151        index_offset += 4;
152        for _ in 0..entry_count {
153            let _ = reader.cpeek_u32_at(index_offset + 2)?;
154            index_offset += 8;
155        }
156
157        reader.cpeek_exact_at(index_offset, &mut tag)?;
158        if &tag != b"EIDX" {
159            return Err(anyhow::anyhow!("Missing EIDX section in ARCC archive"));
160        }
161        index_offset += 4 + 8 * file_count as u64;
162
163        reader.cpeek_exact_at(index_offset, &mut tag)?;
164        if &tag != b"CINF" {
165            return Err(anyhow::anyhow!("Missing CINF section in ARCC archive"));
166        }
167        index_offset += 4;
168
169        let mut entries = Vec::with_capacity(entry_count);
170        for _ in 0..entry_count {
171            index_offset += 6;
172            let name_len = reader.cpeek_u16_at(index_offset)? as usize;
173            let mut name_buf = vec![0u8; name_len];
174            if name_len > 0 {
175                reader.cpeek_exact_at(index_offset + 4, &mut name_buf)?;
176                decrypt_name(&mut name_buf);
177            }
178            index_offset += 6 + name_len as u64;
179            let name = decode_to_string(archive_encoding, &name_buf, true)?;
180            entries.push(HexenHausArccEntry {
181                name,
182                offset: 0,
183                size: 0,
184            });
185        }
186
187        let mut addr_offset = addr_offset;
188        reader.cpeek_exact_at(addr_offset, &mut tag)?;
189        if &tag != b"ADDR" {
190            return Err(anyhow::anyhow!("Missing ADDR section in ARCC archive"));
191        }
192        addr_offset += 4;
193        for entry in &mut entries {
194            entry.offset = reader.cpeek_u64_at(addr_offset + 2)?;
195            addr_offset += 12;
196        }
197
198        for entry in &mut entries {
199            if reader.cpeek_and_equal_at(entry.offset, b"FILE").is_err() {
200                continue;
201            }
202            entry.size = reader.cpeek_u32_at(entry.offset + 0x18)?;
203            entry.offset += 0x22;
204        }
205
206        entries.retain(|entry| entry.size > 0);
207        if entries.is_empty() {
208            return Err(anyhow::anyhow!("ARCC archive contains no files"));
209        }
210
211        Ok(HexenHausArccArchive {
212            reader,
213            entries,
214            _mark: std::marker::PhantomData,
215        })
216    }
217}
218
219impl<'b, T: Read + Seek + std::fmt::Debug + Send + Sync + 'b> Script
220    for HexenHausArccArchive<'b, T>
221{
222    fn default_output_script_type(&self) -> OutputScriptType {
223        OutputScriptType::Json
224    }
225
226    fn default_format_type(&self) -> FormatOptions {
227        FormatOptions::None
228    }
229
230    fn is_archive(&self) -> bool {
231        true
232    }
233
234    fn iter_archive_filename<'a>(
235        &'a self,
236    ) -> Result<Box<dyn Iterator<Item = Result<String>> + 'a>> {
237        Ok(Box::new(
238            self.entries.iter().map(|entry| Ok(entry.name.clone())),
239        ))
240    }
241
242    fn iter_archive_offset<'a>(&'a self) -> Result<Box<dyn Iterator<Item = Result<u64>> + 'a>> {
243        Ok(Box::new(self.entries.iter().map(|entry| Ok(entry.offset))))
244    }
245
246    fn open_file<'a>(&'a self, index: usize) -> Result<Box<dyn ArchiveContent + Send + Sync + 'a>> {
247        if index >= self.entries.len() {
248            return Err(anyhow::anyhow!(
249                "Index out of bounds: {} (total files: {})",
250                index,
251                self.entries.len()
252            ));
253        }
254        let entry = &self.entries[index];
255        let header = self
256            .reader
257            .cpeek_at_vec(entry.offset, (entry.size as usize).min(16))?;
258        Ok(Box::new(Entry {
259            reader: self.reader.clone(),
260            header: entry.clone(),
261            pos: 0,
262            typ: super::detect_script_type(&entry.name, &header),
263        }))
264    }
265}
266
267#[derive(Debug)]
268struct Entry<T: Read + Seek> {
269    header: HexenHausArccEntry,
270    reader: Arc<Mutex<T>>,
271    pos: u64,
272    typ: Option<ScriptType>,
273}
274
275impl<T: Read + Seek + std::fmt::Debug + Send + Sync> ArchiveContent for Entry<T> {
276    fn name(&self) -> &str {
277        &self.header.name
278    }
279
280    fn size(&self) -> Option<u64> {
281        Some(self.header.size as u64)
282    }
283
284    fn script_type(&self) -> Option<&ScriptType> {
285        self.typ.as_ref()
286    }
287
288    fn to_data<'a>(&'a mut self) -> Result<Box<dyn ReadSeek + Send + Sync + 'a>> {
289        Ok(Box::new(self))
290    }
291}
292
293impl<T: Read + Seek> Read for Entry<T> {
294    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
295        let mut reader = self.reader.lock().map_err(|e| {
296            std::io::Error::new(
297                std::io::ErrorKind::Other,
298                format!("Failed to lock mutex: {}", e),
299            )
300        })?;
301        reader.seek(SeekFrom::Start(self.header.offset + self.pos))?;
302        let bytes_read = buf.len().min(self.header.size as usize - self.pos as usize);
303        if bytes_read == 0 {
304            return Ok(0);
305        }
306        let bytes_read = reader.read(&mut buf[..bytes_read])?;
307        self.pos += bytes_read as u64;
308        Ok(bytes_read)
309    }
310}
311
312impl<T: Read + Seek> Seek for Entry<T> {
313    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
314        let new_pos = match pos {
315            SeekFrom::Start(offset) => offset as u64,
316            SeekFrom::End(offset) => {
317                if offset < 0 {
318                    if (-offset) as u64 > self.header.size as u64 {
319                        return Err(std::io::Error::new(
320                            std::io::ErrorKind::InvalidInput,
321                            "Seek from end exceeds file length",
322                        ));
323                    }
324                    self.header.size as u64 - (-offset) as u64
325                } else {
326                    self.header.size as u64 + offset as u64
327                }
328            }
329            SeekFrom::Current(offset) => {
330                if offset < 0 {
331                    if (-offset) as u64 > self.pos {
332                        return Err(std::io::Error::new(
333                            std::io::ErrorKind::InvalidInput,
334                            "Seek from current exceeds current position",
335                        ));
336                    }
337                    self.pos.saturating_sub((-offset) as u64)
338                } else {
339                    self.pos + offset as u64
340                }
341            }
342        };
343        self.pos = new_pos;
344        Ok(self.pos)
345    }
346
347    fn stream_position(&mut self) -> std::io::Result<u64> {
348        Ok(self.pos)
349    }
350}
351
352fn decrypt_name(buf: &mut [u8]) {
353    for byte in buf.iter_mut() {
354        *byte ^= 0x69;
355    }
356}